package com.ar4j.spring;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;


import org.springframework.beans.BeansException;
import org.springframework.beans.FatalBeanException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.FieldCallback;

/**
 * A bean post processor that is able to autowire SpringActiveRecordAdapter instances.
 * A special case of autowiring is needed since Spring's main autowiring process does not detect 
 * generic types.
 */
public class AdapterInjectingBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter implements BeanFactoryAware {
  
  
  private Map<Class<?>, List<Member>> injectedMemberCache = Collections.synchronizedMap(new HashMap<Class<?>, List<Member>>());
  private ReentrantLock memberCacheLock = new ReentrantLock();
  
  private ConfigurableListableBeanFactory beanFactory;

  /**
   * Scan the bean for the @AutowiredAdapter annotation and inject an adapter
   * where needed.
   */
  @Override
  public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
    
    // see if we need to inject a 
    List<Member> toInject = getInjectedMemberList(bean.getClass());
    if(toInject != null && !toInject.isEmpty()) {
      for(Member member : toInject) {
        try {
          if(member instanceof Field) {
             Field field = (Field) member;
             Object value = getAdapterToInject(field, field.getGenericType());
             ReflectionUtils.makeAccessible(field);
             field.set(bean, value);
          } else {
             Method method = (Method) member;
             Object value = getAdapterToInject(method, method.getGenericParameterTypes()[0]);
             ReflectionUtils.makeAccessible(method);
             method.invoke(bean, value);
          }
        } catch(Exception e) {
          throw new FatalBeanException("Could not create and inject a SpringActiveRecordAdapter", e);
        }
      }
    }
    
    return super.postProcessAfterInstantiation(bean, beanName);
  }
  
  /**
   * Keep knowledge of the bean factory for later use
   */
  @Override
  public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
    if(!(beanFactory instanceof ConfigurableListableBeanFactory)) {
      throw new IllegalArgumentException("AdapterInkectingBeanProstProcessor requires a ConfigurableListableBeanFactory or its subclass to operate, got: " + beanFactory.getClass().getName());
    }
    
    this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
  }
  
  /**
   * Creates an adapter for the given type. The type must be a ParamerizedType
   * with one actual type parameter that is a subclass of ActiveRecord.
   * 
   * Detects Qualifier annotations on the member and injects an adapter that uses a factory
   * which has its variables retrieved with the member's qualifier. This feature can be used 
   * to inject the correct DataSource/PlatformTransactionManager into the adapter's factory.
   */
  private SpringActiveRecordAdapter<?> getAdapterToInject(AnnotatedElement member, Type type) {
    
    // make sure the type can be processed
    if(!(type instanceof ParameterizedType)) {
      throw new IllegalArgumentException("Type must be a parameterized type: " + type);
    }
    
    ParameterizedType parameterizedType = (ParameterizedType) type;
    if(parameterizedType.getActualTypeArguments() == null || parameterizedType.getActualTypeArguments().length != 1) {
      throw new IllegalArgumentException("Parameterized type must have one actual type argument: " + parameterizedType);
    }
    
    Class<?> baseClass = (Class<?>) parameterizedType.getActualTypeArguments()[0];
    return SpringActiveRecordAdapterFactory.getSpringActiveRecordAdapter(beanFactory, baseClass, member.getAnnotation(Qualifier.class));
  }
  
  /**
   * Collects a list of members that need injection of an adapter.
   * This version uses a concurrent cache of injected members for the given
   * class. The actual scan is performed by doGetInjectedMemberList(Class)
   */
  private List<Member> getInjectedMemberList(Class<?> clazz) {
    if(!injectedMemberCache.containsKey(clazz)) {
      memberCacheLock.lock();
      try {
        if(!injectedMemberCache.containsKey(clazz)) {
          injectedMemberCache.put(clazz, doGetInjectedMemberList(clazz));
        }
      } finally {
        memberCacheLock.unlock();
      }
    }
    
    return injectedMemberCache.get(clazz);
  }
  
  /**
   * Collects a list of members that need injection of an adapter. (Uncached)
   */
  private List<Member> doGetInjectedMemberList(final Class<?> clazz) {
    final List<Member> out = new ArrayList<Member>();
    
    // field annotation and type scan
    ReflectionUtils.doWithFields(clazz, new FieldCallback() {
      @Override
      public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
        AutowiredAdapter anno = field.getAnnotation(AutowiredAdapter.class);
        if(anno != null) {
          if (!field.getType().isAssignableFrom(SpringActiveRecordAdapter.class)) {
            throw new IllegalStateException("AutowiredAdapter annotation requires a field of type SpringActiveRecordAdapter: " + field);
          }
          
          out.add(field);
        }
      }
    });
    
    // method annotation and type scan
    ReflectionUtils.doWithMethods(clazz, new ReflectionUtils.MethodCallback() {
      public void doWith(Method method) {
        AutowiredAdapter anno = method.getAnnotation(AutowiredAdapter.class);
        if(anno != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
          if (Modifier.isStatic(method.getModifiers())) {
            throw new IllegalStateException("AutowiredAdapter annotation is not supported on static methods");
          }
          
          Class<?>[] types = method.getParameterTypes();
          if (types.length != 1 || !types[0].isAssignableFrom(SpringActiveRecordAdapter.class)) {
            throw new IllegalStateException("AutowiredAdapter annotation requires one argument of type SpringActiveRecordAdapter: " + method);
          }
          
          out.add(method);
        }
      }
    });
    
    return out;
  }

}
